﻿// Copyright (c) 2014 Converter Systems LLC

using Opc.Ua;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

namespace ConverterSystems.ServiceModel
{
    /// <summary>
    /// A lightweight client for working with Opc Ua servers.
    /// </summary>
    public class UaSessionClient : SessionClient
    {
        public EndpointDescription _description;
        private readonly ApplicationConfiguration _appConfiguration;
        private string _endpointUrl;
        private TransportChannelSettings _settings;

        public UaSessionClient(ApplicationConfiguration appConfiguration, string uriOrName, TransportChannelSettings settings = null)
            : base(new TransportChannelBuilder(appConfiguration).Build(uriOrName, ref settings))
        {
            _appConfiguration = appConfiguration;
            _endpointUrl = settings.Description.EndpointUrl;
            _settings = settings;
        }

        public async Task<ActivateSessionResponse> ActivateSessionAsync(ActivateSessionRequest activateSessionRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(activateSessionRequest, true, "ActivateSession");
            ActivateSessionResponse activateSessionResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, activateSessionRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                activateSessionResponse = (ActivateSessionResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(activateSessionRequest, activateSessionResponse, "ActivateSession");
            }
            return activateSessionResponse;
        }

        public async Task<BrowseResponse> BrowseAsync(BrowseRequest browseRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(browseRequest, true, "Browse");
            BrowseResponse browseResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, browseRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                browseResponse = (BrowseResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(browseRequest, browseResponse, "Browse");
            }
            return browseResponse;
        }

        public async Task<BrowseNextResponse> BrowseNextAsync(BrowseNextRequest browseNextRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(browseNextRequest, true, "BrowseNext");
            BrowseNextResponse browseNextResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, browseNextRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                browseNextResponse = (BrowseNextResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(browseNextRequest, browseNextResponse, "BrowseNext");
            }
            return browseNextResponse;
        }

        public async Task<CallResponse> CallAsync(CallRequest callRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(callRequest, true, "Call");
            CallResponse callResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, callRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                callResponse = (CallResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(callRequest, callResponse, "Call");
            }
            return callResponse;
        }

        public override StatusCode Close()
        {
            try
            {
                CloseSession(null, true);
                Trace.TraceInformation("Success closing session with endpoint '{0}'.", this.Endpoint);
            }
            catch (Exception ex)
            {
                Trace.TraceError("Error closing session with endpoint '{0}'. {1}", this.Endpoint, ex.Message);
            }
            return base.Close();
        }

        public async Task<CloseSessionResponse> CloseSessionAsync(CloseSessionRequest closeSessionRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(closeSessionRequest, true, "CloseSession");
            CloseSessionResponse closeSessionResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, closeSessionRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                closeSessionResponse = (CloseSessionResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(closeSessionRequest, closeSessionResponse, "CloseSession");
            }
            return closeSessionResponse;
        }

        public async Task<CreateMonitoredItemsResponse> CreateMonitoredItemsAsync(CreateMonitoredItemsRequest createMonitoredItemsRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(createMonitoredItemsRequest, true, "CreateMonitoredItems");
            CreateMonitoredItemsResponse createMonitoredItemsResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, createMonitoredItemsRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                createMonitoredItemsResponse = (CreateMonitoredItemsResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(createMonitoredItemsRequest, createMonitoredItemsResponse, "CreateMonitoredItems");
            }
            return createMonitoredItemsResponse;
        }

        public async Task<CreateSessionResponse> CreateSessionAsync(CreateSessionRequest createSessionRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(createSessionRequest, true, "CreateSession");
            CreateSessionResponse createSessionResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, createSessionRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                createSessionResponse = (CreateSessionResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(createSessionRequest, createSessionResponse, "CreateSession");
            }
            return createSessionResponse;
        }

        public async Task<CreateSubscriptionResponse> CreateSubscriptionAsync(CreateSubscriptionRequest createSubscriptionRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(createSubscriptionRequest, true, "CreateSubscription");
            CreateSubscriptionResponse createSubscriptionResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, createSubscriptionRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                createSubscriptionResponse = (CreateSubscriptionResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(createSubscriptionRequest, createSubscriptionResponse, "CreateSubscription");
            }
            return createSubscriptionResponse;
        }

        public async Task<DeleteMonitoredItemsResponse> DeleteMonitoredItemsAsync(DeleteMonitoredItemsRequest deleteMonitoredItemsRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(deleteMonitoredItemsRequest, true, "DeleteMonitoredItems");
            DeleteMonitoredItemsResponse deleteMonitoredItemsResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, deleteMonitoredItemsRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                deleteMonitoredItemsResponse = (DeleteMonitoredItemsResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(deleteMonitoredItemsRequest, deleteMonitoredItemsResponse, "DeleteMonitoredItems");
            }
            return deleteMonitoredItemsResponse;
        }

        public async Task<DeleteSubscriptionsResponse> DeleteSubscriptionsAsync(DeleteSubscriptionsRequest deleteSubscriptionsRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(deleteSubscriptionsRequest, true, "DeleteSubscriptions");
            DeleteSubscriptionsResponse deleteSubscriptionsResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, deleteSubscriptionsRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                deleteSubscriptionsResponse = (DeleteSubscriptionsResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(deleteSubscriptionsRequest, deleteSubscriptionsResponse, "DeleteSubscriptions");
            }
            return deleteSubscriptionsResponse;
        }

        public async Task<FindServersResponse> FindServersAsync(FindServersRequest findServersRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(findServersRequest, true, "FindServers");
            FindServersResponse findServersResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, findServersRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                findServersResponse = (FindServersResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(findServersRequest, findServersResponse, "FindServers");
            }
            return findServersResponse;
        }

        public async Task<GetEndpointsResponse> GetEndpointsAsync(GetEndpointsRequest getEndpointsRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(getEndpointsRequest, true, "GetEndpoints");
            GetEndpointsResponse getEndpointsResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, getEndpointsRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                getEndpointsResponse = (GetEndpointsResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(getEndpointsRequest, getEndpointsResponse, "GetEndpoints");
            }
            return getEndpointsResponse;
        }

        public async Task OpenAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            if (Connected) return;
            try
            {
                await UpdateEndpointFromServer();
                var endpoint = TransportChannel.EndpointDescription;
                var identity = new UserIdentity();
                UserIdentityToken identityToken = identity.GetIdentityToken();
                UserTokenPolicy userTokenPolicy = endpoint.FindUserTokenPolicy(identityToken.PolicyId);
                if (userTokenPolicy == null)
                {
                    userTokenPolicy = endpoint.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType);
                    if (userTokenPolicy == null)
                    {
                        throw ServiceResultException.Create(2149515264u, "Endpoint does not support the user identity type provided.", new object[0]);
                    }
                    identityToken.PolicyId = userTokenPolicy.PolicyId;
                }
                string securityPolicyUri = endpoint.SecurityPolicyUri;
                bool requireSecurity = securityPolicyUri != SecurityPolicies.None;
                if (!requireSecurity)
                {
                    requireSecurity = (userTokenPolicy.SecurityPolicyUri != SecurityPolicies.None);
                }
                X509Certificate2 serverCertificate = null;
                byte[] serverCertificateBlob = endpoint.ServerCertificate;
                if (serverCertificateBlob != null && serverCertificateBlob.Length > 0 && requireSecurity)
                {
                    serverCertificate = Utils.ParseCertificateBlob(serverCertificateBlob);
                    _appConfiguration.CertificateValidator.Validate(serverCertificate);
                }
                byte[] clientNonce = new byte[_appConfiguration.SecurityConfiguration.NonceLength];
                new RNGCryptoServiceProvider().GetBytes(clientNonce);
                var clientCertificate = _appConfiguration.SecurityConfiguration.ApplicationCertificate.Find(true);
                byte[] clientCertificateBlob = (clientCertificate != null) ? clientCertificate.RawData : null;
                ApplicationDescription applicationDescription = new ApplicationDescription();
                applicationDescription.ApplicationUri = _appConfiguration.ApplicationUri;
                applicationDescription.ApplicationName = _appConfiguration.ApplicationName;
                applicationDescription.ApplicationType = ApplicationType.Client;
                applicationDescription.ProductUri = _appConfiguration.ProductUri;
                var sessionTimeout = (double)_appConfiguration.ClientConfiguration.DefaultSessionTimeout;
                bool success = false;
                CreateSessionRequest createRequest = null;
                CreateSessionResponse createResponse = null;
                if (securityPolicyUri == SecurityPolicies.None)
                {
                    try
                    {
                        createRequest = new CreateSessionRequest { ClientDescription = applicationDescription, ServerUri = endpoint.Server.ApplicationUri, EndpointUrl = endpoint.EndpointUrl, ClientNonce = clientNonce, RequestedSessionTimeout = sessionTimeout, MaxResponseMessageSize = (uint)TransportChannel.MessageContext.MaxMessageSize };
                        createResponse = await CreateSessionAsync(createRequest, cancellationToken);
                        success = true;
                    }
                    catch (Exception ex)
                    {
                        Utils.Trace("Create session failed with client certificate NULL. " + ex.Message, new object[0]);
                        success = false;
                    }
                }
                if (!success)
                {
                    createRequest = new CreateSessionRequest { ClientDescription = applicationDescription, ServerUri = endpoint.Server.ApplicationUri, EndpointUrl = endpoint.EndpointUrl, ClientNonce = clientNonce, ClientCertificate = clientCertificateBlob, RequestedSessionTimeout = sessionTimeout, MaxResponseMessageSize = (uint)TransportChannel.MessageContext.MaxMessageSize };
                    createResponse = await CreateSessionAsync(createRequest, cancellationToken);
                }
                MessageContext.MaxMessageSize = (int)createResponse.MaxRequestMessageSize;
                SessionCreated(createResponse.SessionId, createResponse.AuthenticationToken);
                if (createResponse.ServerCertificate != null && !Utils.IsEqual(createResponse.ServerCertificate, endpoint.ServerCertificate))
                {
                    throw ServiceResultException.Create(2148663296u, "Server did not return the certificate used to create the secure channel.", new object[0]);
                }
                byte[] dataToVerify = Utils.Append(new byte[][] { clientCertificateBlob, clientNonce });
                if (!SecurityPolicies.Verify(serverCertificate, endpoint.SecurityPolicyUri, dataToVerify, createResponse.ServerSignature))
                {
                    throw ServiceResultException.Create(2153250816u, "Server did not provide a correct signature for the nonce data provided by the client.", new object[0]);
                }
                CertificateValidator certificateValidator = _appConfiguration.CertificateValidator;
                foreach (var current in createResponse.ServerSoftwareCertificates)
                {
                    SoftwareCertificate item = null;
                    ServiceResult serviceResult = SoftwareCertificate.Validate(certificateValidator, current.CertificateData, out item);
                    if (ServiceResult.IsBad(serviceResult))
                    {
                        throw new ServiceResultException(serviceResult);
                    }
                }
                byte[] dataToSign = Utils.Append(new byte[][] { createResponse.ServerCertificate, createResponse.ServerNonce });
                SignatureData clientSignature = SecurityPolicies.Sign(clientCertificate, securityPolicyUri, dataToSign);
                securityPolicyUri = userTokenPolicy.SecurityPolicyUri;
                if (string.IsNullOrEmpty(securityPolicyUri))
                {
                    securityPolicyUri = endpoint.SecurityPolicyUri;
                }
                SignatureData userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri);
                identityToken.Encrypt(serverCertificate, createResponse.ServerNonce, securityPolicyUri);
                var preferredLocales = new StringCollection(new string[] { CultureInfo.CurrentCulture.Name });
                var activateRequest = new ActivateSessionRequest { ClientSignature = clientSignature, LocaleIds = preferredLocales, UserIdentityToken = new ExtensionObject(identityToken), UserTokenSignature = userTokenSignature };
                var activateResponse = await ActivateSessionAsync(activateRequest, cancellationToken);
                await FetchNamespaceTablesAsync(cancellationToken);
                Trace.TraceInformation("Success opening session with endpoint '{0}'. ", _endpointUrl);
            }
            catch (Exception ex)
            {
                Trace.TraceError("Error opening session with endpoint '{0}'. {1}", _endpointUrl, ex.Message);
                try
                {
                    TransportChannel.Close();
                    TransportChannel.Dispose();
                    Trace.TraceInformation("Success closing secure channel with endpoint '{0}'.", _endpointUrl);
                }
                catch (Exception ex2)
                {
                    Trace.TraceError("Error closing secure channel with endpoint '{0}'. {1}", _endpointUrl, ex2.Message);
                }
                finally
                {
                    SessionCreated(null, null);
                }
            }
        }

        public async Task<PublishResponse> PublishAsync(PublishRequest publishRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(publishRequest, true, "Publish");
            PublishResponse publishResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, publishRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                publishResponse = (PublishResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(publishRequest, publishResponse, "Publish");
            }
            return publishResponse;
        }

        public async Task<ReadResponse> ReadAsync(ReadRequest readRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(readRequest, true, "Read");
            ReadResponse readResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, readRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                readResponse = (ReadResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(readRequest, readResponse, "Read");
            }
            return readResponse;
        }

        public async Task<RepublishResponse> RepublishAsync(RepublishRequest republishRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(republishRequest, true, "Republish");
            RepublishResponse republishResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, republishRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                republishResponse = (RepublishResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(republishRequest, republishResponse, "Republish");
            }
            return republishResponse;
        }

        public async Task<SetPublishingModeResponse> SetPublishingModeAsync(SetPublishingModeRequest setPublishingModeRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(setPublishingModeRequest, true, "SetPublishingMode");
            SetPublishingModeResponse setPublishingModeResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, setPublishingModeRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                setPublishingModeResponse = (SetPublishingModeResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(setPublishingModeRequest, setPublishingModeResponse, "SetPublishingMode");
            }
            return setPublishingModeResponse;
        }

        public async Task<TranslateBrowsePathsToNodeIdsResponse> TranslateBrowsePathsToNodeIdsAsync(TranslateBrowsePathsToNodeIdsRequest translateRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(translateRequest, true, "TranslateBrowsePathsToNodeIds");
            TranslateBrowsePathsToNodeIdsResponse translateResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, translateRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                translateResponse = (TranslateBrowsePathsToNodeIdsResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(translateRequest, translateResponse, "TranslateBrowsePathsToNodeIds");
            }
            return translateResponse;
        }

        public async Task<WriteResponse> WriteAsync(WriteRequest writeRequest, CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            UpdateRequestHeader(writeRequest, true, "Write");
            WriteResponse writeResponse = null;
            try
            {
                var serviceResponse = await Task<IServiceResponse>.Factory.FromAsync(TransportChannel.BeginSendRequest, TransportChannel.EndSendRequest, writeRequest, null).ConfigureAwait(false);
                if (serviceResponse == null)
                {
                    throw new ServiceResultException(StatusCodes.BadUnknownResponse);
                }
                ValidateResponse(serviceResponse.ResponseHeader);
                writeResponse = (WriteResponse)serviceResponse;
            }
            finally
            {
                RequestCompleted(writeRequest, writeResponse, "Write");
            }
            return writeResponse;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                Close();
            }
            base.Dispose(disposing);
        }

        private async Task FetchNamespaceTablesAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            ReadValueIdCollection readValueIdCollection = new ReadValueIdCollection();
            readValueIdCollection.Add(new ReadValueId
            {
                NodeId = 2255u,
                AttributeId = 13u
            });
            readValueIdCollection.Add(new ReadValueId
            {
                NodeId = 2254u,
                AttributeId = 13u
            });
            ResponseHeader responseHeader = null;
            DataValueCollection dataValueCollection = null;
            DiagnosticInfoCollection diagnosticInfoCollection = null;
            var readRequest = new ReadRequest { NodesToRead = readValueIdCollection };
            var readResponse = await this.ReadAsync(readRequest, cancellationToken);
            responseHeader = readResponse.ResponseHeader;
            dataValueCollection = readResponse.Results;
            diagnosticInfoCollection = readResponse.DiagnosticInfos;
            ClientBase.ValidateResponse(dataValueCollection, readValueIdCollection);
            ClientBase.ValidateDiagnosticInfos(diagnosticInfoCollection, readValueIdCollection);
            ServiceResult status = ClientBase.ValidateDataValue(dataValueCollection[0], typeof(string[]), 0, diagnosticInfoCollection, responseHeader);
            if (ServiceResult.IsBad(status))
            {
                throw new ServiceResultException(status);
            }
            this.MessageContext.NamespaceUris.Update((string[])dataValueCollection[0].Value);
            status = ClientBase.ValidateDataValue(dataValueCollection[1], typeof(string[]), 1, diagnosticInfoCollection, responseHeader);
            if (ServiceResult.IsBad(status))
            {
                throw new ServiceResultException(status);
            }
            this.MessageContext.ServerUris.Update((string[])dataValueCollection[1].Value);
        }

        private async Task UpdateEndpointFromServer(bool preferPerformance = true, CancellationToken cancellationToken = default(CancellationToken))
        {
            var discoveryUri = new UriBuilder(_endpointUrl);
            var profileUris = new string[0];
            switch (discoveryUri.Scheme)
            {
                case "opc.tcp":
                    profileUris = new[] { Profiles.UaTcpTransport };
                    break;

                case "https":
                    //discoveryUri.Path += discoveryUri.Path.EndsWith("/") ? "discovery" : "/discovery";
                    profileUris = new[] { Profiles.HttpsBinaryTransport, Profiles.HttpsXmlOrBinaryTransport, Profiles.HttpsXmlTransport };
                    break;
            }
            var request = new GetEndpointsRequest { EndpointUrl = _endpointUrl.ToString(), ProfileUris = profileUris };
            var response = await GetEndpointsAsync(request, cancellationToken);

            var desc2 = preferPerformance ? response.Endpoints.OrderBy(ed => profileUris.ToList().IndexOf(ed.TransportProfileUri)).ThenBy(ed => ed.SecurityLevel).FirstOrDefault() : response.Endpoints.OrderBy(ed => profileUris.ToList().IndexOf(ed.TransportProfileUri)).ThenByDescending(ed => ed.SecurityLevel).FirstOrDefault();
            if (desc2 == null)
            {
                throw ServiceResultException.Create(2148728832u, "Server did not return an EndpointDescription that matched the scheme.", new object[0]);
            }
            _settings.Description = desc2;
            _settings.Configuration.UseBinaryEncoding = desc2.EncodingSupport == BinaryEncodingSupport.Optional || desc2.EncodingSupport == BinaryEncodingSupport.Required;
            if (desc2.ServerCertificate != null && desc2.ServerCertificate.Length > 0)
            {
                _settings.ServerCertificate = Utils.ParseCertificateBlob(desc2.ServerCertificate);
            }
            TransportChannel.Reconnect();
        }
    }
}